#!/usr/bin/python3
# -*- coding: utf-8; tab-width: 4; indent-tabs-mode: t -*-

import os
import re
import bbki
import json
import pathlib
import strict_hdds
import strict_hwcfg
import strict_portage
from fm_param import FmConst
from fm_util import Util


class BbkiWrapper:

    def __init__(self, machine, layout=None, mountPointJson=None, bStrict=True):
        self._cfg = BbkiConfig(machine, layout, mountPointJson)
        self._bbkiObj = bbki.Bbki(self._cfg, strict=bStrict)

    def dispose(self):
        return self._bbkiObj.dispose()

    @property
    def cfg(self):
        return self._cfg

    def isStable(self):
        return self._bbkiObj.get_stable_flag()

    def getPendingBootEntry(self):
        return self._bbkiObj.get_pending_boot_entry()

    def sync(self, bootEntrySpec):
        self._bbkiObj.sync(bootEntrySpec)

    def getKernelInstaller(self, bootEntrySpec):
        return self._bbkiObj.obtain_kernel_installer(bootEntrySpec)

    def updateInitramfs(self):
        beList = self._bbkiObj.get_boot_entries()
        if len(beList) == 0:
            raise Exception("no boot entry")
        if len(beList) > 1:
            raise Exception("multiple boot entries")
        updater = self._bbkiObj.obtain_kernel_updater(beList[0])
        try:
            updater.update_initramfs()
        finally:
            updater.dispose()

    def updateBootloader(self):
        beList = self._bbkiObj.get_boot_entries()
        if len(beList) == 0:
            raise Exception("no boot entry")
        if len(beList) > 1:
            raise Exception("multiple boot entries")
        self._bbkiObj.install_bootloader(beList[0], self.getAuxOsInfo(), "")

    def setStable(self, value):
        self._bbkiObj.set_stable_flag(value)

    def check_config(self, auto_fix=False, error_callback=None):
        return self._bbkiObj.check_config(auto_fix, error_callback)

    def check_boot_entry_files(self, auto_fix=False, error_callback=None):
        return self._bbkiObj.check_boot_entry_files(auto_fix, error_callback)

    def clean_boot_entry_files(self, pretend=False):
        return self._bbkiObj.clean_boot_entry_files(pretend)

    def getAuxOsInfo(self):
        if not FmConst.supportOsProber:
            return []

        ret = []
        for line in Util.cmdCall("os-prober").split("\n"):
            itemList = line.split(":")
            if len(itemList) != 4:
                continue
            if itemList[3] == "linux":
                continue

            if itemList[1].endswith("(loader)"):               # for Microsoft Windows quirks
                m = re.fullmatch("(.*?)([0-9]+)", itemList[0])
                osDesc = itemList[1].replace("(loader)", "").strip()
                osPart = "%s%d" % (m.group(1), int(m.group(2)) + 1)
                chain = 4
                ret.append(bbki.AuxOs(osDesc, bbki.AuxOs.Type.MICROSOFT_WINDOWS, osPart, chain))
                continue
            if True:
                ret.append(bbki.AuxOs(itemList[1], bbki.AuxOs.Type.MICROSOFT_WINDOWS, itemList[0], 1))
                continue
        return ret


class BbkiConfig(bbki.ConfigBase):

    def __init__(self, machine, layout, mountPointJson):
        if machine is None:
            assert layout is None and mountPointJson is None
        if layout is not None:
            assert mountPointJson is None
        if mountPointJson is not None:
            assert layout is None

        # self._flags
        if layout is not None:
            self._flags = bbki.Flags.KERNEL | bbki.Flags.INITRAMFS | bbki.Flags.BOOTLOADER | bbki.Flags.BIOS
        elif mountPointJson is not None:
            self._flags = bbki.Flags.KERNEL | bbki.Flags.INITRAMFS
        elif machine is not None:
            self._flags = bbki.Flags.KERNEL
        else:
            self._flags = bbki.Flags(0)

        # self._machine
        self._machine = machine

        # self._offlineMountPoints & self._onlineMountPoints
        if layout is not None:
            self._offlineMountPoints = []
            self._onlineMountPoints = []
            for x in layout.get_mount_commands():
                mp = bbki.HostMountPoint(mountpoint=x.mountpoint, dev_path_or_uuid=x.device, fstype=x.fstype.value, opts=x.opts)
                for disk in mp.underlay_disk.walk("pre-order"):
                    # note: even offline mountpoint also has "write_safety"
                    if isinstance(disk, bbki.HostDiskScsiHdd) or isinstance(disk, bbki.HostDiskNvmeHdd):
                        hddDevPath = Util.devPathDiskOrPartitionToDisk(disk.dev)
                        c = Util.hwcfgGetComponentByUdevDeviceNode(self._machine, strict_hwcfg.ComponentType.HARDDISK, hddDevPath)
                        if c is None:
                            raise Exception("harddisk \"%s\" has no hardware configuration" % (hddDevPath))
                        if c.get_write_safety() == "safe":
                            pass
                        elif c.get_write_safety() == "safe-uncached":
                            disk.should_disable_write_cache = True
                        elif c.get_write_safety() == "unsafe":
                            pass
                        else:
                            assert False
                self._onlineMountPoints.append(mp)
            for mp in self._onlineMountPoints:
                self._offlineMountPoints.append(mp.to_offline_mount_point())
        elif mountPointJson is not None:
            for mp in json.loads(mountPointJson):
                mp = bbki.HostMountPoint(**mp)
                assert not mp.is_online()
                self._offlineMountPoints.append(mp)

        # self._bootMode
        if layout is not None:
            if layout.boot_mode == strict_hdds.BootMode.EFI:
                self._bootMode = bbki.BootMode.EFI
            elif layout.boot_mode == strict_hdds.BootMode.BIOS:
                self._bootMode = bbki.BootMode.BIOS
            else:
                assert False

        # directories
        cfgDir = strict_portage.cfg.PortageConfigDir()
        self._profileOptionsFile = os.path.join(cfgDir.path, "profile", "bbki.options")
        self._cfgOptionsFile = os.path.join(cfgDir.path, "bbki.options")

        # option cache
        self._tOptions = {}

    @property
    def flags(self):
        return self._flags

    def get_fs_layout(self):
        return bbki.FsLayout()

    def get_build_variable(self, var_name):
        self._filltOptions("make")
        return self._tOptions["make"].get(var_name, "")

    def get_mount_points(self):
        return self._offlineMountPoints

    def get_system_init(self):
        return bbki.SystemInit(bbki.SystemInit.Type.SYSTEMD)

    def get_online_mount_points(self):
        return self._onlineMountPoints

    def get_boot_mode(self):
        return self._bootMode

    def get_bootloader_extra_time(self):
        self._filltOptions("bootloader")
        return self._tOptions["bootloader"]["wait-time"]

    def get_kernel_extra_init_cmdline(self):
        return ""

    def _filltOptions(self, key):
        if key in self._tOptions:
            return

        fileList = [
            self._profileOptionsFile,   # step1: use /etc/portage/profile/bbki.*
            self._cfgOptionsFile,       # step2: use /etc/portage/bbki.*
        ]

        if key == "make":
            self._tOptions["make"] = {}

            # determine parallelism parameters
            jobcountMake, loadavg = Util.portageDetectJobCountAndLoadAverage(self._machine)
            self._tOptions["make"]["MAKEOPTS"] = "--jobs=%d --load-average=%d" % (jobcountMake, loadavg)

            return

        if key == "bootloader":
            self._tOptions["bootloader"] = {
                "wait-time": 0,
            }

            # read from config file
            for filePath in fileList:
                try:
                    data = Util.file2json(filePath)
                    self._tOptions["bootloader"].update(data.get("bootloader", {}))
                except FileNotFoundError:
                    pass

            # check
            try:
                v = int(self._tOptions["bootloader"]["wait-time"])
                if not (0 <= v <= 3600):
                    raise Exception("invalid value of BBKI option bootloader/wait-time")
            except ValueError:
                raise Exception("invalid value of BBKI option bootloader/wait-time")

            return

        assert False


class BbkiCfgFileUtil:

    @staticmethod
    def checkKernelCfgFileContent(buf):
        tlist = buf.rstrip("\n").split()
        if tlist[0] != "linux":
            return False
        if len(tlist) > 2:
            return False
        if len(tlist) == 2:
            m = re.fullmatch('version="(.*)"', tlist[1])
            if m is None:
                return False
            verstr = m.group(1)
            if verstr.startswith(">=") or verstr.startswith("<="):
                verstr = verstr[2:]
            elif verstr.startswith(">") or verstr.startswith("<"):
                verstr = verstr[1:]
            partList = verstr.split("-")
            try:
                verList = [int(x) for x in partList[0].split(".")]
            except ValueError:
                return False
            if not (len(verList) == 3 and all([0 <= x <= 99 for x in verList])):
                return False
        return True

    @staticmethod
    def checkKernelAddonCfgFileContent(buf):
        return True

    @classmethod
    def parseKernelCfgFile(cls, filepath):
        buf = pathlib.Path(filepath).read_text()

        if not cls.checkKernelCfgFileContent(buf):
            raise Exception("invalid content in %s" % (filepath))

        ret = cls._parse(filepath, buf, "kernel")
        if len(ret) != 1:
            raise Exception("invalid content in %s" % (filepath))

        kernelType, kernelParams, bAdd = ret[0]
        assert bAdd

        return (kernelType, kernelParams)

    @classmethod
    def parseKernelAddonCfgFile(cls, filepath):
        buf = pathlib.Path(filepath).read_text()
        return cls._parse(filepath, buf, "kernel addon")

    @classmethod
    def _parse(cls, filepath, buf, myName):
        ret = []

        for i, line in enumerate(buf.split("\n")):
            line = line.strip()
            if line == "" or line.startswith("#"):
                continue

            if line.startswith("-"):
                line = line[1:]
                if " " in line:
                    raise Exception("invalid line %d in %s" % (i + 1, filepath))
                ret.append((line, False))
                continue

            m = re.fullmatch("(\\S+)( +(.*))?", line)
            if m is None:
                raise Exception("invalid line %d in %s" % (i + 1, filepath))

            if m.group(2) is None:
                ret.append((m.group(1), {}, True))
            else:
                ret.append((m.group(1), cls._parseParametersInFile(myName, m.group(3)), True))

        return ret

    @staticmethod
    def _parseParametersInFile(myName, s):
        ret = {}

        for item in s.split(" "):
            tlist = item.split("=")
            if len(tlist) == 1:
                ret[item] = None
            elif len(tlist) == 2:
                if tlist[1].startswith("\"") or tlist[1].endswith("\""):
                    if not (tlist[1].startswith("\"") and tlist[1].endswith("\"")):
                        raise Exception("invalid %s parameter %s" % (myName, tlist[0]))
                    tlist[1] = tlist[1][1:-1]
                elif tlist[1].startswith("\'") and tlist[1].endswith("\'"):
                    if not (tlist[1].startswith("\'") and tlist[1].endswith("\'")):
                        raise Exception("invalid %s parameter %s" % (myName, tlist[0]))
                    tlist[1] = tlist[1][1:-1]
                else:
                    pass
                ret[tlist[0]] = tlist[1]
            else:
                raise Exception("invalid %s parameter" % (myName))

        return ret
